home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 10900 / 10900.xpi / modules / service.js < prev    next >
Text File  |  2010-01-12  |  51KB  |  1,376 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is Personas.
  15.  *
  16.  * The Initial Developer of the Original Code is Mozilla.
  17.  * Portions created by the Initial Developer are Copyright (C) 2007
  18.  * the Initial Developer. All Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *   Chris Beard <cbeard@mozilla.org>
  22.  *   Myk Melez <myk@mozilla.org>
  23.  *
  24.  * Alternatively, the contents of this file may be used under the terms of
  25.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  26.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  27.  * in which case the provisions of the GPL or the LGPL are applicable instead
  28.  * of those above. If you wish to allow use of your version of this file only
  29.  * under the terms of either the GPL or the LGPL, and not to allow others to
  30.  * use your version of this file under the terms of the MPL, indicate your
  31.  * decision by deleting the provisions above and replace them with the notice
  32.  * and other provisions required by the GPL or the LGPL. If you do not delete
  33.  * the provisions above, a recipient may use your version of this file under
  34.  * the terms of any one of the MPL, the GPL or the LGPL.
  35.  *
  36.  * ***** END LICENSE BLOCK ***** */
  37.  
  38. let EXPORTED_SYMBOLS = ["PersonaService", "PERSONAS_EXTENSION_ID"];
  39.  
  40. const Cc = Components.classes;
  41. const Ci = Components.interfaces;
  42. const Cr = Components.results;
  43. const Cu = Components.utils;
  44.  
  45. // modules that come with Firefox
  46. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  47. // LightweightThemeManager may not be not available (Firefox < 3.6 or Thunderbird)
  48. try { Cu.import("resource://gre/modules/LightweightThemeManager.jsm"); }
  49. catch (e) { LightweightThemeManager = null; }
  50.  
  51. // modules that are generic
  52. Cu.import("resource://personas/modules/JSON.js");
  53. Cu.import("resource://personas/modules/Observers.js");
  54. Cu.import("resource://personas/modules/Preferences.js");
  55. Cu.import("resource://personas/modules/StringBundle.js");
  56. Cu.import("resource://personas/modules/URI.js");
  57.  
  58. const PERSONAS_EXTENSION_ID = "personas@christopher.beard";
  59.  
  60. const COOKIE_INITIAL_PERSONA = "initial_persona";
  61. const COOKIE_USER = "PERSONA_USER";
  62.  
  63. let PersonaService = {
  64.   THUNDERBIRD_ID: "{3550f703-e582-4d05-9a08-453d09bdfdc6}",
  65.   FIREFOX_ID:     "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
  66.  
  67.   //**************************************************************************//
  68.   // Shortcuts
  69.  
  70.   // Access to extensions.personas.* preferences.  To access other preferences,
  71.   // call the Preferences module directly.
  72.   get _prefs() {
  73.     delete this._prefs;
  74.     return this._prefs = new Preferences("extensions.personas.");
  75.   },
  76.  
  77.   get _strings() {
  78.     delete this._strings;
  79.     return this._strings = new StringBundle("chrome://personas/locale/personas.properties");
  80.   },
  81.  
  82.   get appInfo() {
  83.     delete this.appInfo;
  84.     return this.appInfo = Cc["@mozilla.org/xre/app-info;1"].
  85.                            getService(Ci.nsIXULAppInfo).
  86.                            QueryInterface(Ci.nsIXULRuntime);
  87.   },
  88.  
  89.   get extension() {
  90.     delete this.extension;
  91.  
  92.     if (this.appInfo.ID == this.FIREFOX_ID) {
  93.       return this.extension = Cc["@mozilla.org/fuel/application;1"].
  94.                               getService(Ci.fuelIApplication).
  95.                               extensions.get(PERSONAS_EXTENSION_ID);
  96.     }
  97.  
  98.     // If STEEL provides a FUEL-compatible extIExtension interface
  99.     // in Thunderbird, return it here.
  100.  
  101.     return this.extension = null;
  102.   },
  103.  
  104.  
  105.   //**************************************************************************//
  106.   // Initialization & Destruction
  107.  
  108.   _init: function() {
  109.     // Observe quit so we can destroy ourselves.
  110.     Observers.add("quit-application", this.onQuitApplication, this);
  111.     // Observe the "cookie-changed" topic to load the favorite personas when
  112.     // the user signs in.
  113.     Observers.add("cookie-changed", this.onCookieChanged, this);
  114.     // Observe the "lightweight-theme-changed" to sync the add-on with the
  115.     // lightweight theme manager.
  116.     Observers.add("lightweight-theme-changed",
  117.                   this.onLightweightThemeChanged, this);
  118.  
  119.     this._prefs.observe("useTextColor",   this.onUseColorChanged,     this);
  120.     this._prefs.observe("useAccentColor", this.onUseColorChanged,     this);
  121.     this._prefs.observe("selected",       this.onSelectedModeChanged, this);
  122.  
  123.     // Get the initial persona specified by a cookie, if any.  The gallery
  124.     // sets this when users download Personas from the Details page
  125.     // for a specific persona, so that the user sees that persona when they
  126.     // install the extension.  We only check for a cookie on first run,
  127.     // because it would be expensive to traverse cookies every time, and since
  128.     // the gallery only sets the initial persona cookie on installation.
  129.     let personaFromCookie;
  130.     if (this.extension && this.extension.firstRun)
  131.       personaFromCookie = this._getPersonaFromCookie();
  132.  
  133.     // Change to the initial persona if preferences indicate that a persona
  134.     // should be active but they don't specify the persona that is active.
  135.     // This normally happens only on first run, although it could theoretically
  136.     // happen at other times.
  137.     //
  138.     // The initial persona is either the persona specified by a cookie (set by
  139.     // the gallery), the one specified by the extensions.personas.initial
  140.     // preference (set by a distribution.ini file for a BYOB/bundle/distributon
  141.     // build), or the Groovy Blue persona which is hardcoded into this code.
  142.     //
  143.     // NOTE: the logic here is carefully designed to achieve the correct outcome
  144.     // under a variety of circumstances and should be changed with caution!
  145.     // See bug 513765 and bug 503300 for some details on why it works this way.
  146.     //
  147.     if (this._prefs.get("selected") == "current" && !this._prefs.get("current")) {
  148.       if (personaFromCookie)
  149.         this.changeToPersona(personaFromCookie);
  150.       else if (this._prefs.has("initial"))
  151.         this.changeToPersona(JSON.parse(this._prefs.get("initial")));
  152.       else {
  153.         this.changeToPersona({
  154.           "id":"33",
  155.           "name":"Groovy Blue",
  156.           "accentcolor":"#6699ff",
  157.           "textcolor":"#07188d",
  158.           "header":"3\/3\/33\/tbox-groovy_blue.jpg",
  159.           "footer":"3\/3\/33\/stbar-groovy_blue.jpg",
  160.           "category":"Abstract",
  161.           "description":null,
  162.           "author":"Lee.Tom",
  163.           "username":"Lee.Tom",
  164.           "detailURL":"http:\/\/www.getpersonas.com\/persona\/33",
  165.           "headerURL":"http:\/\/www.getpersonas.com\/static\/3\/3\/33\/tbox-groovy_blue.jpg",
  166.           "footerURL":"http:\/\/www.getpersonas.com\/static\/3\/3\/33\/stbar-groovy_blue.jpg",
  167.           "previewURL":"http:\/\/www.getpersonas.com\/static\/3\/3\/33\/preview.jpg",
  168.           "iconURL":"http:\/\/www.getpersonas.com\/static\/3\/3\/33\/preview_small.jpg",
  169.           "dataurl":"data:image\/png;base64,\/9j\/4AAQSkZJRgABAQEASABIAAD\/2" +
  170.                     "wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQIC" +
  171.                     "AQECAQEBAgICAgICAgICAQICAgICAgICAgL\/2wBDAQEBAQEBAQEBAQ" +
  172.                     "ECAQEBAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA" +
  173.                     "gICAgICAgICAgICAgL\/wAARCAAQABADAREAAhEBAxEB\/8QAFwAAAw" +
  174.                     "EAAAAAAAAAAAAAAAAABAcICv\/EACAQAAIDAAEFAQEAAAAAAAAAAAME" +
  175.                     "AQIFBgcIERIjEyX\/xAAZAQACAwEAAAAAAAAAAAAAAAADCAQFBwn\/x" +
  176.                     "AAlEQACAQMCBgMBAAAAAAAAAAABAgMEERIAIQUGExQiMQcyM1L\/2gA" +
  177.                     "MAwEAAhEDEQA\/AL9p21L52peGctNVXkGbWsGZhNZlXBWo0sHVSVucA" +
  178.                     "9BryNibVm9Iq3ahWLengV+g03yTMyl+6aV1YE45YmTZsGsCVxBUjx+p" +
  179.                     "sovuFgi5IjlgRUhI7RjYHyJy93Y5EKxBW+9wrKN9wjsft65bGnnPK4+" +
  180.                     "kTguZqqKcdJstEzbuzyTdErUtSFqszqY4uR7lQGdQ+q13rML1vWIrN1" +
  181.                     "U\/I1N2FTG6oKipx6pUlijxRM+ICsRE8scbuqSDEqCvsAiRJyNRvUBH" +
  182.                     "qcq9I5XdFTxaNbKcmxdWMcYRSt\/IqokK5a0NdQOicaI0cEnGi6eWiW" +
  183.                     "TKvqghltgpT1GECpZj+DYQmXSG97SAtxBkXibk9Ul4TzXMk3dSV6wMg" +
  184.                     "a6MSDshAuB+xYiNQVs4F8jZV1u9RwmCKGdKWneSorMFJGPTQGRWkYlj" +
  185.                     "ZVAyKqQRckAG+i+M9rHGtTUI31CU3dwGWRG+GN7lTWmhe4VpCxNRnr+" +
  186.                     "y47Wm9qxSwigg5A0YJT6yDifOtS0MS8Km6JmDdZelgASbgeyrEfUtdg" +
  187.                     "4VXKoSU0bhlDJStUxz00KgWWORAuboP6IAIBIywKgKSRd7Btf\/2Q=="
  188.         });
  189.       }
  190.     }
  191.  
  192.     let timerManager = Cc["@mozilla.org/updates/timer-manager;1"].
  193.                        getService(Ci.nsIUpdateTimerManager);
  194.  
  195.     // Load cached personas data
  196.     this.loadDataFromCache();
  197.  
  198.     // Refresh data, then set a timer to refresh it periodically.
  199.     // This isn't quite right, since we always load data on startup, even if
  200.     // we've recently refreshed it.  And the timer that refreshes data ignores
  201.     // the data load on startup, so if it's been more than the timer interval
  202.     // since a user last started her browser, we load the data twice:
  203.     // once because the browser starts and once because the refresh timer fires.
  204.     this.refreshData();
  205.     let dataRefreshCallback = {
  206.       _svc: this,
  207.       notify: function(timer) { this._svc._refreshDataWithMetrics() }
  208.     };
  209.     timerManager.registerTimer("personas-data-refresh-timer",
  210.                                dataRefreshCallback,
  211.                                86400 /* in seconds == one day */);
  212.  
  213.     // Refresh the current persona once per day.  We only do this for
  214.     // Thunderbird, since Firefox's built-in LightweightThemeManager does this
  215.     // for Firefox nowadays.
  216.     if (this.appInfo.ID == this.THUNDERBIRD_ID) {
  217.       let personaRefreshCallback = {
  218.         _svc: this,
  219.         notify: function(timer) { this._svc._refreshPersona() }
  220.       };
  221.       timerManager.registerTimer("personas-persona-refresh-timer",
  222.                                  personaRefreshCallback,
  223.                                  86400 /* in seconds == one day */);
  224.     }
  225.  
  226.     // Load cached favorite personas
  227.     this.loadFavoritesFromCache();
  228.  
  229.     this.refreshFavorites();
  230.     // Refresh the favorite personas once per day.
  231.     let favoritesRefreshCallback = {
  232.       _svc: this,
  233.       notify: function(timer) { this._svc.refreshFavorites() }
  234.     };
  235.     timerManager.registerTimer("personas-favorites-refresh-timer",
  236.                                favoritesRefreshCallback,
  237.                                86400 /* in seconds == one day */);
  238.  
  239.     this._rotationTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  240.     this.onSelectedModeChanged();
  241.   },
  242.  
  243.   _destroy: function() {
  244.     Observers.remove("cookie-changed", this.onCookieChanged, this);
  245.     Observers.remove("lightweight-theme-changed",
  246.                      this.onLightweightThemeChanged, this);
  247.  
  248.     this._prefs.ignore("useTextColor",   this.onUseColorChanged,     this);
  249.     this._prefs.ignore("useAccentColor", this.onUseColorChanged,     this);
  250.     this._prefs.ignore("selected",       this.onSelectedModeChanged, this);
  251.   },
  252.  
  253.  
  254.   //**************************************************************************//
  255.   // XPCOM Plumbing
  256.  
  257.   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
  258.                                          Ci.nsIDOMEventListener,
  259.                                          Ci.nsITimerCallback]),
  260.  
  261.  
  262.   //**************************************************************************//
  263.   // Data Retrieval
  264.  
  265.   _makeRequest: function(url, loadCallback, headers, aIsBinary) {
  266.     let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
  267.  
  268.     request = request.QueryInterface(Ci.nsIDOMEventTarget);
  269.     request.addEventListener("load", loadCallback, false);
  270.  
  271.     request = request.QueryInterface(Ci.nsIXMLHttpRequest);
  272.     request.open("GET", url, true);
  273.  
  274.     // Force the request to include cookies even though this chrome code
  275.     // is seen as a third-party, so the server knows the user for which we are
  276.     // requesting favorites (or anything else user-specific in the future).
  277.     // This only works in Firefox 3.6; in Firefox 3.5 the request will instead
  278.     // fail to send cookies if the user has disabled third-party cookies.
  279.     try {
  280.       request.channel.QueryInterface(Ci.nsIHttpChannelInternal).
  281.         forceAllowThirdPartyCookie = true;
  282.     }
  283.     catch(ex) { /* user is using Firefox 3.5 */ }
  284.  
  285.     if (headers)
  286.       for (let header in headers)
  287.         request.setRequestHeader(header, headers[header]);
  288.  
  289.     if (aIsBinary)
  290.       request.overrideMimeType('text/plain; charset=x-user-defined');
  291.  
  292.     request.send(null);
  293.   },
  294.  
  295.   /**
  296.    * Refresh data. This method gets called on demand (including on startup)
  297.    * and retrieves data without passing any additional information about
  298.    * the selected persona and the application (that information is only included
  299.    * in the daily retrieval so we can get consistent daily statistics from it
  300.    * no matter how many times a user starts the application in a given day).
  301.    */
  302.   refreshData: function() {
  303.     let url = this.dataURL + "index_" + this._prefs.get("data.version") + ".json";
  304.     let t = this;
  305.     this._makeRequest(url, function(evt) { t.onDataLoadComplete(evt) });
  306.   },
  307.  
  308.   /**
  309.    * Refresh data, providing metrics on persona usage in the process.
  310.    * This method gets called approximately once per day on a cross-session timer
  311.    * (provided Firefox is run every day), updates the version of the data
  312.    * that is currently in memory, and passes information about the selected
  313.    * persona and the host application to the server for statistical analysis
  314.    * (f.e. figuring out which personas are the most popular).
  315.    */
  316.   _refreshDataWithMetrics: function() {
  317.     let appInfo     = Cc["@mozilla.org/xre/app-info;1"].
  318.                       getService(Ci.nsIXULAppInfo);
  319.     let xulRuntime  = Cc["@mozilla.org/xre/app-info;1"].
  320.                       getService(Ci.nsIXULRuntime);
  321.  
  322.     // Calculate the amount of time (in hours) since the persona was last changed.
  323.     let duration = "";
  324.     if (this._prefs.has("persona.lastChanged"))
  325.       duration = Math.round((new Date() - new Date(parseInt(this._prefs.get("persona.lastChanged")))) / 1000 / 60 / 60);
  326.  
  327.     // This logic is based on ExtensionManager::_updateLocale.
  328.     let locale;
  329.     try {
  330.       if (Preferences.get("intl.locale.matchOS")) {
  331.         let localeSvc = Cc["@mozilla.org/intl/nslocaleservice;1"].
  332.                         getService(Ci.nsILocaleService);
  333.         locale = localeSvc.getLocaleComponentForUserAgent();
  334.       }
  335.       else
  336.         throw "set locale in the catch block";
  337.     }
  338.     catch (ex) {
  339.       locale = Preferences.get("general.useragent.locale");
  340.     }
  341.  
  342.     let params = {
  343.       type:       this.selected,
  344.       id:         this.currentPersona ? this.currentPersona.id : "",
  345.       duration:   duration,
  346.       appID:      appInfo.ID,
  347.       appVersion: appInfo.version,
  348.       appLocale:  locale,
  349.       appOS:      xulRuntime.OS,
  350.       appABI:     xulRuntime.XPCOMABI
  351.     };
  352.  
  353.     //dump("params: " + [name + "=" + encodeURIComponent(params[name]) for (name in params)].join("&") + "\n");
  354.  
  355.     let url = this.dataURL + "index_" + this._prefs.get("data.version") + ".json?" +
  356.               [name + "=" + encodeURIComponent(params[name]) for (name in params)].join("&");
  357.     let t = this;
  358.     this._makeRequest(url, function(evt) { t.onDataLoadComplete(evt) });
  359.   },
  360.  
  361.   onDataLoadComplete: function(aEvent) {
  362.     let request = aEvent.target;
  363.  
  364.     // XXX Try to reload again sooner?
  365.     if (request.status != 200)
  366.       throw("problem loading data: " + request.status + " - " + request.statusText);
  367.  
  368.     this.personas = JSON.parse(request.responseText);
  369.  
  370.     // Cache the response
  371.     let cacheDirectory =
  372.       FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
  373.     FileUtils.writeFile(cacheDirectory, "personas.json", request.responseText);
  374.   },
  375.  
  376.   /**
  377.    * Attempts to load this.personas from the cached file in cache/personas.json
  378.    */
  379.   loadDataFromCache : function() {
  380.     let cacheDirectory =
  381.       FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
  382.     let data = FileUtils.readFile(cacheDirectory, "personas.json");
  383.  
  384.     try { this.personas = JSON.parse(data); }
  385.     catch (e) {
  386.       // Could not load from cached data, file empty or does not exist perhaps
  387.       return;
  388.     }
  389.  
  390.     // Now that we have data, pick a new random persona.  Currently, this is
  391.     // the only time we pick a random persona besides when the user selects
  392.     // the "Random From [category]" menuitem, which means the user gets a new
  393.     // random persona each time they start the browser.
  394.     if (this.selected == "random") {
  395.       this.currentPersona = this._getRandomPersonaFromCategory(this.category);
  396.       this._prefs.reset("persona.lastRefreshed");
  397.       this._notifyPersonaChanged(this.currentPersona);
  398.     }
  399.   },
  400.  
  401.   /**
  402.    * Makes a request to obtain the favorite personas json. This occurs only if
  403.    * a user is currenly signed in.
  404.    */
  405.   refreshFavorites : function() {
  406.     // Only refresh if the user is signed in at the moment.
  407.     if (this.isUserSignedIn) {
  408.       let url =
  409.         "http://" + this._prefs.get("host") + "/gallery/All/Favorites?json=1";
  410.       let t = this;
  411.       this._makeRequest(url, function(evt) { t.onFavoritesLoadComplete(evt) });
  412.     }
  413.   },
  414.  
  415.   /**
  416.    * Handles the response from the refreshFavorites method. Loads the favorite
  417.    * personas list.
  418.    * @param aEvent The Http request event object.
  419.    */
  420.   onFavoritesLoadComplete : function(aEvent) {
  421.     let request = aEvent.target;
  422.  
  423.     if (request.status != 200)
  424.       throw("problem loading favorites: " + request.status + " - " + request.statusText);
  425.  
  426.     try {
  427.       this.favorites = JSON.parse(request.responseText);
  428.     }
  429.     catch(ex) {
  430.       Cu.reportError("error parsing favorites data; perhaps you are using " +
  431.                      "Firefox 3.5 and have disabled third-party cookies?");
  432.       return;
  433.     }
  434.  
  435.     // Cache the response
  436.     let cacheDirectory =
  437.       FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
  438.     FileUtils.writeFile(cacheDirectory, "favorites.json", request.responseText);
  439.   },
  440.  
  441.   /**
  442.    * Attempts to load this.favorites from the cached file in
  443.    * cache/favorites.json
  444.    */
  445.   loadFavoritesFromCache : function() {
  446.     let cacheDirectory =
  447.       FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
  448.     let data = FileUtils.readFile(cacheDirectory, "favorites.json");
  449.  
  450.     try { this.favorites = JSON.parse(data); }
  451.     catch (e) {
  452.       // Could not load from cached data, file empty or does not exist perhaps
  453.     }
  454.   },
  455.  
  456.   /**
  457.    * Adds the given persona to the favorites list. If the persona is already
  458.    * in the list then it is replaced.
  459.    * @param aPersona The persona object to be added.
  460.    */
  461.   addFavoritePersona : function(aPersona) {
  462.     // Make sure the favorites list exists.
  463.     if (!this.favorites)
  464.       this.favorites = [];
  465.  
  466.     let i = this._findPersonaInArray(aPersona, this.favorites);
  467.     if (i >= 0)
  468.       this.favorites[i] = aPersona;
  469.     else
  470.       this.favorites.push(aPersona);
  471.   },
  472.  
  473.   /**
  474.    * Removes the given persona from the favorites list, if found.
  475.    * @param aPersona The persona object to be removed.
  476.    */
  477.   removeFavoritePersona : function(aPersona) {
  478.     // Abort if the favorites list hasn't been created.
  479.     if (!this.favorites)
  480.       return;
  481.  
  482.     let i = this._findPersonaInArray(aPersona, this.favorites);
  483.     if (i >= 0)
  484.       this.favorites.splice(i, 1);
  485.   },
  486.  
  487.   _refreshPersona: function() {
  488.     // Only refresh the persona if the user selected a specific persona with an
  489.     // ID and update URL.  If the user selected a random persona, we'll change
  490.     // it the next time we refresh the directory; if the user selected
  491.     // the default persona, we don't need to refresh it, as it doesn't change;
  492.     // if the user selected a custom persona (which doesn't have an ID), it's
  493.     // not clear what refreshing it would mean; and if the persona doesn't have
  494.     // an update URL, then we don't have a way to refresh it.
  495.     if (this.selected != "current" ||
  496.         !this.currentPersona ||
  497.         !this.currentPersona.id ||
  498.         !this.currentPersona.updateURL)
  499.       return;
  500.  
  501.     let headers = {};
  502.  
  503.     if (this._prefs.has("persona.lastRefreshed")) {
  504.       let date = new Date(parseInt(this._prefs.get("persona.lastRefreshed")));
  505.       headers["If-Modified-Since"] = DateUtils.toRFC1123(date);
  506.     }
  507.  
  508.     let t = this;
  509.     this._makeRequest(this.currentPersona.updateURL,
  510.                       function(evt) { t.onPersonaLoadComplete(evt) },
  511.                       headers);
  512.   },
  513.  
  514.   onPersonaLoadComplete: function(event) {
  515.     let request = event.target;
  516.  
  517.     // 304 means the file we requested has not been modified since the
  518.     // If-Modified-Since date we specified, so there's nothing to do.
  519.     if (request.status == 304) {
  520.       //dump("304 - the persona has not been modified\n");
  521.       return;
  522.     }
  523.  
  524.     if (request.status != 200)
  525.       throw("problem refreshing persona: " + request.status + " - " + request.statusText);
  526.  
  527.     let persona = JSON.parse(request.responseText);
  528.  
  529.     // If the persona we're refreshing is no longer the selected persona,
  530.     // then cancel the refresh (otherwise we'd undo whatever changes the user
  531.     // has just made).
  532.     if (this.selected != "current" || !this.currentPersona ||
  533.         this.currentPersona.id != persona.id) {
  534.       //dump("persona " + persona.id + "(" + persona.name + ") no longer the current persona; ignoring refresh\n");
  535.       return;
  536.     }
  537.  
  538.     // If the version strings are identical, the persona hasn't changed.
  539.     if ((persona.version || "") == (this.currentPersona.version || ""))
  540.       return;
  541.  
  542.     // Set the current persona to the updated version we got from the server,
  543.     // and notify observers about the change.
  544.     this.currentPersona = persona;
  545.     this._notifyPersonaChanged(this.currentPersona);
  546.  
  547.     // Record when this refresh took place so the next refresh only looks
  548.     // for changes since this refresh.
  549.     // Note: we set the preference to a string value because preferences
  550.     // can't hold large enough integer values.
  551.     this._prefs.set("persona.lastRefreshed", new Date().getTime().toString());
  552.   },
  553.  
  554.  
  555.   //**************************************************************************//
  556.   // Implementation
  557.  
  558.   // The JSON feed of personas retrieved from the server.
  559.   // Loaded upon service initialization and reloaded periodically thereafter.
  560.   personas: null,
  561.  
  562.   // The JSON feed of favorite personas retrieved from the server.
  563.   favorites: null,
  564.  
  565.   /**
  566.    * extensions.personas.selected: the type of persona that the user selected;
  567.    * possible values are default (the default Firefox theme), random (a random
  568.    * persona from a category), current (the value of this.currentPersona), and
  569.    * randomFavorite (a random persona from the favorite list).
  570.    */
  571.   get selected()        { return this._prefs.get("selected") },
  572.   set selected(newVal)  {        this._prefs.set("selected", newVal) },
  573.  
  574.   /**
  575.    * extensions.personas.current: the current persona
  576.    */
  577.   get currentPersona() {
  578.     let current = this._prefs.get("current");
  579.     if (current) {
  580.       try       { return JSON.parse(current) }
  581.       catch(ex) { Cu.reportError("error getting current persona: " + ex) }
  582.     }
  583.     return null;
  584.   },
  585.   set currentPersona(newVal) {
  586.     try {
  587.       this._prefs.set("current", JSON.stringify(newVal));
  588.       this._cachePersonaImages(newVal);
  589.     }
  590.     catch(ex) { Cu.reportError("error setting current persona: " + ex) }
  591.   },
  592.  
  593.   /**
  594.    * extensions.personas.category: the category from which to pick a random
  595.    * persona.
  596.    */
  597.   get category()        { return this._prefs.get("category") },
  598.   set category(newVal)  {        this._prefs.set("category", newVal) },
  599.  
  600.   /**
  601.    * The URL at which the static data is located.
  602.    */
  603.   get dataURL() {
  604.     return "http://" + this._prefs.get("host") + "/static/";
  605.   },
  606.  
  607.   /**
  608.    * extensions.personas.custom: the custom persona.
  609.    */
  610.   get customPersona() {
  611.     let custom = this._prefs.get("custom");
  612.     if (custom) {
  613.       try       { return JSON.parse(custom) }
  614.       catch(ex) { Cu.reportError("error getting custom persona: " + ex) }
  615.     }
  616.     return null;
  617.   },
  618.   set customPersona(newVal) {
  619.     try       { this._prefs.set("custom", JSON.stringify(newVal)) }
  620.     catch(ex) { Cu.reportError("error setting custom persona: " + ex) }
  621.   },
  622.  
  623.   /**
  624.    * Notifies the persona changes or uses the lightweight theme manager
  625.    * functionality for this purpose (if available)
  626.    * @param aPersona the persona to be set as current if the lightweight theme
  627.    * manager is available
  628.    */
  629.   _notifyPersonaChanged : function(aPersona) {
  630.     if (LightweightThemeManager)
  631.       LightweightThemeManager.currentTheme = aPersona;
  632.     else
  633.       Observers.notify("personas:persona:changed");
  634.   },
  635.  
  636.   changeToDefaultPersona: function() {
  637.     this.selected = "default";
  638.     this._prefs.set("persona.lastChanged", new Date().getTime().toString());
  639.     this._notifyPersonaChanged(null);
  640.   },
  641.  
  642.   changeToRandomPersona: function(category) {
  643.     this.category = category;
  644.     this.currentPersona = this._getRandomPersonaFromCategory(category);
  645.     this.selected = "random";
  646.     this._prefs.set("persona.lastChanged", new Date().getTime().toString());
  647.     this._notifyPersonaChanged(this.currentPersona);
  648.   },
  649.  
  650.   changeToRandomFavoritePersona : function() {
  651.     if (this.favorites && this.favorites.length > 0 && this.isUserSignedIn) {
  652.       this.currentPersona = this._getRandomPersonaFromArray(this.favorites);
  653.       this.selected = "randomFavorite";
  654.       this._prefs.set("persona.lastChanged", new Date().getTime().toString());
  655.       this._notifyPersonaChanged(this.currentPersona);
  656.     }
  657.   },
  658.  
  659.   changeToPersona: function(persona) {
  660.     // Check whether the persona is in the favorites or the recent lists,
  661.     // in which case the change-notification should not be shown.
  662.     let recent = this.getRecentPersonas();
  663.     let favorites = this.favorites;
  664.     let inRecent =
  665.       (recent && recent.some(function(v) v.id == persona.id));
  666.     let inFavorites =
  667.       (favorites && favorites.some(function(v) v.id == persona.id));
  668.  
  669.     this.currentPersona = persona;
  670.     this._addPersonaToRecent(persona);
  671.     this.selected = "current";
  672.     this._prefs.reset("persona.lastRefreshed");
  673.     this._prefs.set("persona.lastChanged", new Date().getTime().toString());
  674.     this._notifyPersonaChanged(this.currentPersona);
  675.  
  676.     // Show the notification if the selected persona is not in the favorite or
  677.     // recent lists, is not a custom persona and its author or username is not null.
  678.     // In this case we make sure at least one of these two fields is not null
  679.     // to prevent bug 526788.
  680.     if (!inRecent && !inFavorites && !persona.custom && (persona.author || persona.username))
  681.       this._showPersonaChangeNotification();
  682.   },
  683.  
  684.   /**
  685.    * Reverts the current persona to the previously selected persona, if
  686.    * available
  687.    */
  688.   revertToPreviousPersona : function() {
  689.     let undonePersonaId = this.currentPersona.id;
  690.     let previousPersona = this._prefs.get("lastselected1");
  691.     if (previousPersona) {
  692.       this.currentPersona = JSON.parse(previousPersona);
  693.       this._revertRecent();
  694.       this.selected = "current";
  695.  
  696.       if (LightweightThemeManager) {
  697.         // forget the lightweight theme too
  698.         LightweightThemeManager.forgetUsedTheme(undonePersonaId);
  699.         LightweightThemeManager.currentTheme = this.currentPersona;
  700.       }
  701.       else
  702.         this.resetPersona();
  703.     }
  704.   },
  705.  
  706.   /**
  707.    * Shows a notification displaying the currently selected persona and button
  708.    * to revert the changes.
  709.    */
  710.   _showPersonaChangeNotification : function() {
  711.     // Obtain most recent window and its notification box
  712.     let wm =
  713.       Cc["@mozilla.org/appshell/window-mediator;1"].
  714.         getService(Ci.nsIWindowMediator);
  715.  
  716.     let notificationBox;
  717.     switch (this.appInfo.ID) {
  718.       case this.FIREFOX_ID:
  719.         notificationBox = wm.getMostRecentWindow("navigator:browser").
  720.                           getBrowser().getNotificationBox();
  721.         break;
  722.       case this.THUNDERBIRD_ID:
  723.         notificationBox = wm.getMostRecentWindow("mail:3pane").
  724.                           document.getElementById("mail-notification-box");
  725.         break;
  726.       default:
  727.         throw "unknown application ID " + this.appInfo.ID;
  728.     }
  729.  
  730.     // If there is another notification of the same kind already, remove it.
  731.     let oldNotification =
  732.       notificationBox.getNotificationWithValue("lwtheme-install-notification");
  733.     if (oldNotification)
  734.       notificationBox.removeNotification(oldNotification);
  735.  
  736.     let message = this._strings.get("notification.personaWasSelected",
  737.                                     [this.currentPersona.name,
  738.                                      (this.currentPersona.author ?
  739.                                       this.currentPersona.author :
  740.                                       this.currentPersona.username)]);
  741.  
  742.     let revertButton = {
  743.       label     : this._strings.get("notification.revertButton.label"),
  744.       accesskey : this._strings.get("notification.revertButton.accesskey"),
  745.       popup     : null,
  746.       callback  : function() { PersonaService.revertToPreviousPersona(); }
  747.     };
  748.  
  749.     notificationBox.appendNotification(
  750.       message, "lwtheme-install-notification", null,
  751.       notificationBox.PRIORITY_INFO_MEDIUM, [ revertButton ] );
  752.   },
  753.  
  754.   /**
  755.    * Looks for the given persona in the given array and returns its index.
  756.    * @param aPersona The persona to be found.
  757.    * @param aPersonaArray The array in which to look for the persona.
  758.    * @return The index of the persona in the array; -1 if not found.
  759.    */
  760.   _findPersonaInArray : function(aPersona, aPersonaArray) {
  761.     for (let i = 0; i < aPersonaArray.length; i++) {
  762.       if (aPersonaArray[i].id == aPersona.id)
  763.         return i;
  764.     }
  765.     return -1;
  766.   },
  767.  
  768.   _getRandomPersonaFromArray : function(aPersonaArray) {
  769.     // Get a random item from the list, trying up to five times to get one
  770.     // that is different from the currently-selected item in the category
  771.     // (if any).  We use Math.floor instead of Math.round to pick a random
  772.     // number because the JS reference says Math.round returns a non-uniform
  773.     // distribution
  774.     // <http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Math:random#Examples>.
  775.     if (aPersonaArray && aPersonaArray.length > 0) {
  776.       let randomIndex, randomItem;
  777.       for (let i = 0; i < 5; i++) {
  778.         randomIndex = Math.floor(Math.random() * aPersonaArray.length);
  779.         randomItem = aPersonaArray[randomIndex];
  780.         if (!this.currentPersona || randomItem.id != this.currentPersona.id)
  781.           break;
  782.       }
  783.  
  784.       return randomItem;
  785.     }
  786.     return this.currentPersona;
  787.   },
  788.  
  789.   _getRandomPersonaFromCategory: function(aCategoryName) {
  790.     // If we have the list of categories, use it to pick a random persona
  791.     // from the selected category.
  792.     if (this.personas && this.personas.categories) {
  793.       let personas;
  794.       for each (let category in this.personas.categories) {
  795.         if (aCategoryName == category.name) {
  796.           personas = category.personas;
  797.           break;
  798.         }
  799.       }
  800.  
  801.       return this._getRandomPersonaFromArray(personas);
  802.     }
  803.     return this.currentPersona;
  804.   },
  805.  
  806.   /**
  807.    * Obtains the list of recently selected personas, parsed from the
  808.    * "lastselected" preferences.
  809.    * @param aHowMany (Optional, default 4) How many personas to obtain.
  810.    * @return The list of recent personas.
  811.    */
  812.   getRecentPersonas : function(aHowMany) {
  813.     if (!aHowMany)
  814.       aHowMany = 4;
  815.  
  816.     // Parse the list from the preferences
  817.     let personas = [];
  818.     for (let i = 0; i < aHowMany; i++) {
  819.       if (this._prefs.has("lastselected" + i)) {
  820.         try {
  821.           personas.push(JSON.parse(this._prefs.get("lastselected" + i)));
  822.         }
  823.         catch(ex) {}
  824.       }
  825.     }
  826.  
  827.     return personas;
  828.   },
  829.  
  830.   _addPersonaToRecent: function(persona) {
  831.     let personas = this.getRecentPersonas();
  832.  
  833.     // Remove personas with the same ID (i.e. don't allow the recent persona
  834.     // to appear twice on the list).  Afterwards, we'll add the recent persona
  835.     // to the list in a way that makes it the most recent one.
  836.     if (persona.id)
  837.       personas = personas.filter(function(v) !v.id || v.id != persona.id);
  838.  
  839.     // Make the new persona the most recent one.
  840.     personas.unshift(persona);
  841.  
  842.     // Note: at this point, there might be five personas on the list, four
  843.     // that we parsed from preferences and the one we're now adding. But we
  844.     // only serialize the first four back to preferences, so the oldest one
  845.     // drops off the end of the list.
  846.  
  847.     // We store five in case we need to revert changes in the list alter on,
  848.     // even though only four will be displayed.
  849.     for (let i = 0; i < 5; i++) {
  850.       if (i < personas.length)
  851.         this._prefs.set("lastselected" + i, JSON.stringify(personas[i]));
  852.       else
  853.         this._prefs.reset("lastselected" + i);
  854.     }
  855.   },
  856.  
  857.   /**
  858.    * Removes the most recent persona from the recent list, and leaves the
  859.    * following four personas as the most recent.
  860.    */
  861.   _revertRecent: function() {
  862.     // Create a new list of recent personas, removing the first one, but
  863.     // including the 5th (hidden) one.
  864.     let personas = this.getRecentPersonas(5);
  865.     personas.shift();
  866.  
  867.     // Serialize the list of recent personas.
  868.     for (let i = 0; i < 5; i++) {
  869.       if (i < personas.length)
  870.         this._prefs.set("lastselected" + i, JSON.stringify(personas[i]));
  871.       else
  872.         this._prefs.reset("lastselected" + i);
  873.     }
  874.   },
  875.  
  876.   onUseColorChanged: function() {
  877.     // Notify observers that the persona has changed so the change in whether
  878.     // or not to use the text or accent color will get applied.  The persona
  879.     // hasn't really changed, but doing this has the desired effect without any
  880.     // known unwanted side effects.
  881.     Observers.notify("personas:persona:changed");
  882.   },
  883.  
  884.   previewingPersona: null,
  885.  
  886.   /**
  887.    * Display the given persona temporarily.  Useful for showing users who are
  888.    * browsing the directory of personas what a given persona will look like
  889.    * when selected, f.e. on mouseover.  Consumers who call this method should
  890.    * call resetPersona when the preview ends, f.e. on mouseout.
  891.    */
  892.   previewPersona: function(persona) {
  893.     if (LightweightThemeManager)
  894.       LightweightThemeManager.previewTheme(persona);
  895.     else {
  896.       this.previewingPersona = persona;
  897.       Observers.notify("personas:persona:changed");
  898.     }
  899.   },
  900.  
  901.   /**
  902.    * Stop previewing a persona.
  903.    */
  904.   resetPersona: function() {
  905.     if (LightweightThemeManager)
  906.       LightweightThemeManager.resetPreview();
  907.     else {
  908.       this.previewingPersona = null;
  909.       Observers.notify("personas:persona:changed");
  910.     }
  911.   },
  912.  
  913.   /**
  914.    * Gets the persona specified by the initial_persona cookie.
  915.    */
  916.   _getPersonaFromCookie: function() {
  917.     let authorizedHosts = this._prefs.get("authorizedHosts").split(/[, ]+/);
  918.     let cookieManager =
  919.       Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
  920.     let cookieEnu = cookieManager.enumerator;
  921.     let selectedCookie = null;
  922.  
  923.     while (cookieEnu.hasMoreElements()) {
  924.       let cookie = cookieEnu.getNext().QueryInterface(Ci.nsICookie);
  925.  
  926.       // XXX: Remove the initial dot from the host name, if any, before it is
  927.       // compared against the authorized hosts. This fixes the bug reported in
  928.       // bug 492392 that getpersonas.com and www.getpersonas.com cookies
  929.       // imported from IE have the cookie host .getpersonas.com, so they didn't
  930.       // match any of the authorized hosts.
  931.       let cookieHost = cookie.host.replace(/^\./, "");
  932.  
  933.       if (cookie.name == COOKIE_INITIAL_PERSONA &&
  934.           authorizedHosts.some(function(v) v == cookieHost)) {
  935.  
  936.         // There could be more than one "initial_persona" cookie. The cookie
  937.         // with latest expiration time is selected.
  938.         if (null == selectedCookie ||
  939.             cookie.expires > selectedCookie.expires) {
  940.           selectedCookie = cookie;
  941.         }
  942.  
  943.         cookieManager.remove(cookie.host, cookie.name, cookie.path, false);
  944.       }
  945.     }
  946.  
  947.     if (selectedCookie)
  948.       return JSON.parse(decodeURIComponent(selectedCookie.value));
  949.  
  950.     return null;
  951.   },
  952.  
  953.   /**
  954.    * Whether or not a user is signed in, determined by the presence of a cookie
  955.    * named "PERSONA_USER".
  956.    * @return True if signed in, false otherwise.
  957.    */
  958.   get isUserSignedIn() {
  959.     let cookieManager =
  960.       Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2);
  961.  
  962.     let userCookie = {
  963.       QueryInterface: XPCOMUtils.generateQI([Ci.nsICookie2, Ci.nsICookie]),
  964.       host: this._prefs.get("host"),
  965.       path: "/",
  966.       name: COOKIE_USER
  967.     };
  968.  
  969.     return cookieManager.cookieExists(userCookie);
  970.   },
  971.  
  972.   //**************************************************************************//
  973.   // Lightweight Themes - Personas synchronization
  974.  
  975.   /**
  976.    * Updates the add-on to reflect the changes from the Tools - Add-ons - Themes
  977.    * dialog. If a lightweight theme is set, it is also set as the add-on's current
  978.    * persona. If a regular theme is set, the current persona is set to "default".
  979.    */
  980.   onLightweightThemeChanged: function() {
  981.     let currentTheme = LightweightThemeManager.currentTheme;
  982.     if (currentTheme && currentTheme.id != this.currentPersona.id)
  983.       this.changeToPersona(currentTheme);
  984.     else if (!currentTheme && this.selected != "default")
  985.       this.changeToDefaultPersona();
  986.   },
  987.  
  988.   //**************************************************************************//
  989.   // Random Rotation
  990.  
  991.   _rotationTimer : null,
  992.  
  993.   /**
  994.    * Checks the current value of the "selected" preference and sets the
  995.    * rotation timer accordingly.
  996.    */
  997.   onSelectedModeChanged: function() {
  998.     this._rotationTimer.cancel();
  999.  
  1000.     let selectedMode = this.selected;
  1001.  
  1002.     if (selectedMode == "random" || selectedMode == "randomFavorite") {
  1003.       let interval = this._prefs.get("rotationInterval") * 1000;
  1004.       let that = this;
  1005.  
  1006.       this._rotationTimer.initWithCallback(
  1007.         { notify: function(aTimer) { that._rotatePersona(); } },
  1008.         interval, Ci.nsITimer.TYPE_REPEATING_SLACK);
  1009.  
  1010.       this._rotatePersona();
  1011.     }
  1012.   },
  1013.  
  1014.   /**
  1015.    * Changes the current persona to a random persona of the same category (while
  1016.    * in "random" mode) or a random persona from he favorite list (while in
  1017.    * "randomFavorite" mode).
  1018.    */
  1019.   _rotatePersona : function() {
  1020.     switch (this.selected) {
  1021.       case "random":
  1022.         this.changeToRandomPersona(this.category);
  1023.         break;
  1024.       case "randomFavorite":
  1025.         this.changeToRandomFavoritePersona();
  1026.         break;
  1027.     }
  1028.   },
  1029.  
  1030.   //**************************************************************************//
  1031.   // Persona Images Caching
  1032.  
  1033.   /**
  1034.    * Caches the header and footer images of the given persona inside the
  1035.    * directory [profile]/personas/cache/[persona.id]. It removes all other
  1036.    * existing persona directories before doing so.
  1037.    * @param aPersona The persona for which to cache the images.
  1038.    */
  1039.   _cachePersonaImages : function(aPersona) {
  1040.     let cacheDirectory =
  1041.       FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
  1042.  
  1043.     // Remove all other subdirectories in the cache directory
  1044.     // XXX: In the future, if we want to keep more than one persona cached
  1045.     // this step would be removed.
  1046.     let subdirs = FileUtils.getDirectoryEntries(cacheDirectory);
  1047.     for (let i = 0; i < subdirs.length; i++) {
  1048.       if (subdirs[i].isDirectory())
  1049.         subdirs[i].remove(true);
  1050.     }
  1051.  
  1052.     // Create directory for the given persona
  1053.     let personaDir = FileUtils.getDirectory(cacheDirectory, aPersona.id);
  1054.  
  1055.     // Save header if specified.
  1056.     let header = aPersona.headerURL || aPersona.header;
  1057.     if (header) {
  1058.       let headerURI = URI.get(header, null, URI.get(this.dataURL)).
  1059.                       QueryInterface(Ci.nsIURL);
  1060.       let headerCallback = function(aEvent) {
  1061.         let request = aEvent.target;
  1062.         // Save only if the folder still exists (Could have been deleted already)
  1063.         if (request.status == 200 && personaDir.exists()) {
  1064.           FileUtils.writeBinaryFile(
  1065.             personaDir.clone(),
  1066.             "header" + "." + headerURI.fileExtension,
  1067.             request.responseText);
  1068.         }
  1069.       };
  1070.       this._makeRequest(headerURI.spec, headerCallback, null, true);
  1071.     }
  1072.  
  1073.     // Save footer if specified.
  1074.     let footer = aPersona.footerURL || aPersona.footer;
  1075.     if (footer) {
  1076.       let footerURI = URI.get(footer, null, URI.get(this.dataURL)).
  1077.                       QueryInterface(Ci.nsIURL);
  1078.       let footerCallback = function(aEvent) {
  1079.         let request = aEvent.target;
  1080.         // Save only if the folder still exists (Could have been deleted already)
  1081.         if (request.status == 200 && personaDir.exists()) {
  1082.           FileUtils.writeBinaryFile(
  1083.             personaDir.clone(),
  1084.             "footer" + "." + footerURI.fileExtension,
  1085.             request.responseText);
  1086.         }
  1087.       };
  1088.       this._makeRequest(footerURI.spec, footerCallback, null, true);
  1089.     }
  1090.   },
  1091.  
  1092.   /**
  1093.    * Obtains the cached images of the given persona. This are stored in the
  1094.    * _cachePersonaImages method under the directory
  1095.    * [profile]/personas/cache/[persona.id].
  1096.    * @param aPersona The persona for which to look the cached images.
  1097.    * @return An object with "header" and "footer" properties, each containing
  1098.    * the file URL of the image. Null otherwise.
  1099.    */
  1100.   getCachedPersonaImages : function(aPersona) {
  1101.     let cacheDirectory =
  1102.       FileUtils.getDirectory(FileUtils.getPersonasDirectory(), "cache");
  1103.  
  1104.     let personaDir = FileUtils.getDirectory(cacheDirectory, aPersona.id, true);
  1105.     if (personaDir.exists()) {
  1106.  
  1107.       let headerFile = personaDir.clone();
  1108.       let footerFile = personaDir.clone();
  1109.  
  1110.       let headerFileExtension =
  1111.         URI.get(aPersona.headerURL || aPersona.header, null, URI.get(this.dataURL)).
  1112.         QueryInterface(Ci.nsIURL).fileExtension;
  1113.  
  1114.       let footerFileExtension =
  1115.         URI.get(aPersona.footerURL || aPersona.footer, null, URI.get(this.dataURL)).
  1116.         QueryInterface(Ci.nsIURL).fileExtension;
  1117.  
  1118.       headerFile.append("header" + "." + headerFileExtension);
  1119.       footerFile.append("footer" + "." + footerFileExtension);
  1120.  
  1121.       if (headerFile.exists() && footerFile.exists()) {
  1122.         let ios =
  1123.           Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
  1124.  
  1125.         let headerURI = ios.newFileURI(headerFile);
  1126.         let footerURI = ios.newFileURI(footerFile);
  1127.  
  1128.         return {
  1129.           header : headerURI.spec,
  1130.           footer : footerURI.spec
  1131.         };
  1132.       }
  1133.     }
  1134.     return null;
  1135.   },
  1136.  
  1137.   /**
  1138.    * Monitors changes in cookies. If the modified cookie is the Personas session
  1139.    * cookie, then the favorites are refreshed (if the user is signed in).
  1140.    * @param aCookie The cookie that has been added, changed or removed.
  1141.    */
  1142.   onCookieChanged : function(aCookie) {
  1143.     aCookie.QueryInterface(Ci.nsICookie);
  1144.  
  1145.     if (aCookie.name == COOKIE_USER &&
  1146.         aCookie.host == this._prefs.get("host")) {
  1147.       this.refreshFavorites();
  1148.     }
  1149.   },
  1150.  
  1151.   onQuitApplication: function() {
  1152.     Observers.remove("quit-application", this.onQuitApplication, this);
  1153.     this._destroy();
  1154.   }
  1155. };
  1156.  
  1157. let DateUtils = {
  1158.   /**
  1159.    * Returns the number as a string with a 0 prepended to it if it contains
  1160.    * only one digit, for formats like ISO 8601 that require two digit month,
  1161.    * day, hour, minute, and second values (f.e. so midnight on January 1, 2009
  1162.    * becomes 2009:01:01T00:00:00Z instead of 2009:1:1T0:0:0Z, which would be
  1163.    * invalid).
  1164.    */
  1165.   _pad: function(number) {
  1166.     return (number >= 0 && number <= 9) ? "0" + number : "" + number;
  1167.   },
  1168.  
  1169.   /**
  1170.    * Format a date per ISO 8601, in particular the subset described in
  1171.    * http://www.w3.org/TR/NOTE-datetime, which is recommended for date
  1172.    * interchange on the internet.
  1173.    *
  1174.    * Example: 1994-11-06T08:49:37Z
  1175.    *
  1176.    * @param   date  {Date}    the date to format
  1177.    * @returns       {String}  the date formatted per ISO 8601
  1178.    */
  1179.   toISO8601: function(date) {
  1180.     let year = date.getUTCFullYear();
  1181.     let month = this._pad(date.getUTCMonth() + 1);
  1182.     let day = this._pad(date.getUTCDate());
  1183.     let hours = this._pad(date.getUTCHours());
  1184.     let minutes = this._pad(date.getUTCMinutes());
  1185.     let seconds = this._pad(date.getUTCSeconds());
  1186.     return year + "-" + month + "-" + day + "T" +
  1187.            hours + ":" + minutes + ":" + seconds + "Z";
  1188.   },
  1189.  
  1190.   /**
  1191.    * Format a date per RFC 1123, which is the standard for HTTP headers.
  1192.    *
  1193.    * Example: Sun, 06 Nov 1994 08:49:37 GMT
  1194.    *
  1195.    * I'd love to use Datejs here, but its Date::toString formatting method
  1196.    * doesn't convert dates to their UTC equivalents before formatting them,
  1197.    * resulting in incorrect output (since RFC 1123 requires dates to be
  1198.    * in UTC), so instead I roll my own.
  1199.    *
  1200.    * @param   date  {Date}    the date to format
  1201.    * @returns       {String}  the date formatted per RFC 1123
  1202.    */
  1203.   toRFC1123: function(date) {
  1204.     let dayOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][date.getUTCDay()];
  1205.     let day = this._pad(date.getUTCDate());
  1206.     let month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][date.getUTCMonth()];
  1207.     let year = date.getUTCFullYear();
  1208.     let hours = this._pad(date.getUTCHours());
  1209.     let minutes = this._pad(date.getUTCMinutes());
  1210.     let seconds = this._pad(date.getUTCSeconds());
  1211.     return dayOfWeek + ", " + day + " " + month + " " + year + " " +
  1212.            hours + ":" + minutes + ":" + seconds + " GMT";
  1213.   }
  1214. };
  1215.  
  1216. let FileUtils = {
  1217.   /**
  1218.    * Gets the [profile]/personas directory.
  1219.    * @return The reference to the personas directory (nsIFile).
  1220.    */
  1221.   getPersonasDirectory : function() {
  1222.     let directoryService =
  1223.       Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
  1224.     let dir = directoryService.get("ProfD", Ci.nsIFile);
  1225.  
  1226.     return this.getDirectory(dir, "personas");
  1227.   },
  1228.  
  1229.   /**
  1230.    * Gets a reference to a directory (nsIFile) specified by the given name,
  1231.    * located inside the given parent directory.
  1232.    * @param aParentDirectory The parent directory of the directory to obtain.
  1233.    * @param aDirectoryName The name of the directory to obtain.
  1234.    * @param aDontCreate (Optional) Whether or not to create the directory if it
  1235.    * does not exist.
  1236.    * @return The reference to the directory (nsIFile).
  1237.    */
  1238.   getDirectory : function(aParentDirectory, aDirectoryName, aDontCreate) {
  1239.     let dir = aParentDirectory.clone();
  1240.     try {
  1241.       dir.append(aDirectoryName);
  1242.       if (!dir.exists() || !dir.isDirectory()) {
  1243.         if (!aDontCreate) {
  1244.           // read and write permissions to owner and group, read-only for others.
  1245.           dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0774);
  1246.         }
  1247.       }
  1248.     }
  1249.     catch (ex) {
  1250.       Cu.reportError("Could not create '" + aDirectoryName + "' directory");
  1251.       dir = null;
  1252.     }
  1253.     return dir;
  1254.   },
  1255.  
  1256.   /**
  1257.    * Gets an array of the entries (nsIFile) found in the given directory.
  1258.    * @param aDirectory The directory from which to obtain the entries.
  1259.    * @return The array of entries.
  1260.    */
  1261.   getDirectoryEntries : function(aDirectory) {
  1262.     let entries = [];
  1263.     try {
  1264.       let enu = aDirectory.directoryEntries;
  1265.       while (enu.hasMoreElements()) {
  1266.         let entry = enu.getNext().QueryInterface(Ci.nsIFile);
  1267.         entries.push(entry);
  1268.       }
  1269.     }
  1270.     catch (ex) {
  1271.       Cu.reportError("Could not read entries of directory");
  1272.     }
  1273.     return entries;
  1274.   },
  1275.  
  1276.   /**
  1277.    * Reads the contents of a text file located at the given directory.
  1278.    * @param aDirectory The directory in which the file is read from (nsIFile)
  1279.    * @param aFileName The name of the file to be read.
  1280.    * @return The contents of the file (string), if any.
  1281.    */
  1282.   readFile : function(aDirectory, aFileName) {
  1283.     let data = "";
  1284.  
  1285.     try {
  1286.       let file = aDirectory.clone();
  1287.       file.append(aFileName);
  1288.  
  1289.       if (file.exists()) {
  1290.         let fstream =
  1291.           Cc["@mozilla.org/network/file-input-stream;1"].
  1292.             createInstance(Ci.nsIFileInputStream);
  1293.         fstream.init(file, -1, 0, 0);
  1294.  
  1295.         let cstream =
  1296.           Cc["@mozilla.org/intl/converter-input-stream;1"].
  1297.             createInstance(Ci.nsIConverterInputStream);
  1298.         cstream.init(fstream, "UTF-8", 0, 0);
  1299.  
  1300.         let (str = {}) {
  1301.           // read the whole file
  1302.           while (cstream.readString(-1, str))
  1303.             data += str.value;
  1304.         }
  1305.         cstream.close(); // this also closes fstream
  1306.       }
  1307.     }
  1308.     catch (ex) {
  1309.       Cu.reportError("Could not read file " + aFileName);
  1310.     }
  1311.  
  1312.     return data;
  1313.   },
  1314.  
  1315.   /**
  1316.    * Writes a text file in the given directory. If the file already exists it is
  1317.    * overwritten.
  1318.    * @param aDirectory The directory in which the file will be written (nsIFile).
  1319.    * @param aFileName The name of the file to be written.
  1320.    * @param aData The contents of the file.
  1321.    */
  1322.   writeFile : function(aDirectory, aFileName, aData) {
  1323.     try {
  1324.       let file = aDirectory.clone();
  1325.       file.append(aFileName);
  1326.  
  1327.       let foStream =
  1328.         Cc["@mozilla.org/network/file-output-stream;1"].
  1329.           createInstance(Ci.nsIFileOutputStream);
  1330.       // flags are write, create, truncate
  1331.       foStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
  1332.  
  1333.       let converter =
  1334.         Cc["@mozilla.org/intl/converter-output-stream;1"].
  1335.           createInstance(Ci.nsIConverterOutputStream);
  1336.       converter.init(foStream, "UTF-8", 0, 0);
  1337.       converter.writeString(aData);
  1338.       converter.close(); // this also closes foStream
  1339.     }
  1340.     catch (ex) {
  1341.       Cu.reportError("Could not write file " + aFileName);
  1342.     }
  1343.   },
  1344.  
  1345.   /**
  1346.    * Writes a binary file in the given directory. If the file already exists it
  1347.    * is overwritten.
  1348.    * @param aDirectory The directory in which the file will be written (nsIFile).
  1349.    * @param aFileName The name of the file to be written.
  1350.    * @param aData The binary contents of the file.
  1351.    */
  1352.   writeBinaryFile : function(aDirectory, aFileName, aData) {
  1353.     try {
  1354.       let file = aDirectory.clone();
  1355.       file.append(aFileName);
  1356.  
  1357.       let stream =
  1358.         Cc["@mozilla.org/network/safe-file-output-stream;1"].
  1359.           createInstance(Ci.nsIFileOutputStream);
  1360.       // Flags are: write, create, truncate
  1361.       stream.init(file, 0x04 | 0x08 | 0x20, 0600, 0);
  1362.  
  1363.       stream.write(aData, aData.length);
  1364.       if (stream instanceof Ci.nsISafeOutputStream)
  1365.         stream.finish();
  1366.       else
  1367.         stream.close();
  1368.     }
  1369.     catch (ex) {
  1370.       Cu.reportError("Could not write binary file " + aFileName);
  1371.     }
  1372.   }
  1373. };
  1374.  
  1375. PersonaService._init();
  1376.